package com.vitria.m3oui.test.px;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;



class Node{
	String activity = null;
	Long timestamp = 0L;
	ArrayList<String> attributes = null;
	
	Node(String act, ArrayList<String> attrs, Long time){
		this.attributes = attrs;
		this.timestamp = time;
		this.activity = act;
	}
	
	Node copy(){
		return new Node(this.activity, this.attributes, this.timestamp);
	}
}

class Path{
	
	ArrayList<Node> path;
	String instance_id;
	
	Path(Node head, String instance_id){
		this.path = new ArrayList<Node>();
		path.add(head);
		this.instance_id = instance_id;
	}
	
	void insert(Node insert_node){
		path.add(insert_node);
		for(int i=path.size()-1; i>=1; i--){
			Node currentNode = path.get(i);			
			Node prevNode = path.get(i-1);  
			if(prevNode.timestamp < currentNode.timestamp || prevNode.timestamp.equals(currentNode.timestamp)){
				break;
			} else {
				Node temp = currentNode.copy();
				path.set(i, prevNode);
				path.set(i-1, temp);
			}
		}
	}
	
	boolean hasNode(String nodeName){
		for(Node node:path){
			if(node.activity.equals(nodeName))
				return true;
		}
		return false;
	}
	
	boolean beginWith(String headName){
		Node head = this.path.get(0);
		if(head.activity.equals(headName))
			return true;
		else
			return false;
	}
	
	void cutPathByGivenHead(String newHeadName){
		int index = path.size()-1;
		int last = index;
		for(int i = last; i >= 0; i--){
			Node node = path.get(i);
			if(node.activity.equals(newHeadName)){
				if(i < index)
					index = i;
			}
		}
		for(int j = index - 1; j >= 0; j--){
			path.remove(j);
		}
	}
	
	@Override
	public String toString(){
		String result = path.get(0).activity;
		for(int i=1; i<path.size(); i++)
			result +=" -> "+path.get(i).activity;
		return result;
	}
	
	@Override
	public boolean equals(Object tgt) { 
		if(this == tgt)
			return true;
		if(tgt instanceof Path){
			Path pathObj = (Path) tgt;
			if(path != null){
				if(pathObj.path != null){
					if(path.size() != pathObj.path.size()){
						return false;
					} else {
						for(int i=0; i<path.size(); i++){
							if(!path.get(i).activity.equals(pathObj.path.get(i).activity))
								return false;
						}
						return true;
					}
				}else{
					return false;
				}
			} else if(pathObj.path == null){
				return true;
			} else {
				return false;
			}		
		} else {
			return false;
		}
	}
	
	@Override
	public int hashCode() { 
		int result =17;
		for(int i=0; i<path.size(); i++)
			result += (i+1)*path.get(i).activity.hashCode();
		return result;
	}
	
}

class Duration{
	double count = 0.0;
	long cost = 0L;
	
	Duration(long time){
		cost += time;
		count = 1.0;
	}
	
	void increase(long time){
		count++;
		cost+=time;
	}
}

class Frequency{
	double count = 0.0;
	
	Frequency(){
		count = 1;
	}
	
	void increase(){
		count++;
	}
}

class ActivityDetailsOnPath{
	Duration duration = null;
	Frequency frequency = null;
	
	ActivityDetailsOnPath(Long time){
		duration = new  Duration(time);
		frequency = new Frequency();
	}
	
	void increase(Long time){
		duration.increase(time);
		frequency.increase();
	}
	
}

class PathDetail{
	double instance_count = 0.0;
	long timecost = 0L;
	int event_count = 0;
	ArrayList<String> instanceIDs = new ArrayList<String>();
	
	Map<String, ActivityDetailsOnPath> activityDetails = null;
}

public class StatisticHelper {
	private static Log log_ = LogFactory.getLog(StatisticHelper.class);
	private static Map<String, ArrayList<ArrayList<String>>> samples = new HashMap<String, ArrayList<ArrayList<String>>>();
	private static String RESULT = "Expect Results:\n\nProcess Summary:\n" +
			"Event Processed:{event_num}\n" +
			"Process Instances:{process_num}\n" +
			"Process Average Duraion:{process_avg}\n" +
			"Unique Paths:{path_num}\n\n";
	
	private static String PREDICTIVE_RESULT = "\nPredictive:\n\nProcess Outcomes:\n";
	
	private static Long YEAR_THRESHOLD = 3600*24*365*1000L;
	private static Long DAY_THRESHOLD = 3600*24*1000L;
	private static Long HOUR_THRESHOLD = 3600*1000L;
	private static Long MIN_THRESHOLD = 60*1000L;
	private static Long SEC_THRESHOLD = 1000L;
	private static String YEAR_UNIT = "y";
	private static String DAY_UNIT = "d";
	private static String HOUR_UNIT = "h";
	private static String MIN_UNIT = "m";
	private static String SEC_UNIT = "s";
	private static String MILISEC_UNIT = "ms";
	
	private int id_index = 0;
	private int timestamp_index = 1;
	private int activity_index = 2;
	private String time_pattern = "MM/dd/yyyy HH:mm:ss";
	
	private boolean filterPath = false;
	private String filterStartNodeName = null;
	private String filterHasNodeName = null;
	private boolean filterAttr = false;
	private Map<Integer, String> filterAttributes = null;
	
	Map<String, Path> allPaths = new HashMap<String, Path>();
	Map<Path, PathDetail> pathDetails = new HashMap<Path, PathDetail>();
	//Record the path info, activities lead time and begin time
	ArrayList<ArrayList<String>> trendAndDurationData;
	//Record every process instance duration and begin time and the path info
	ArrayList<ArrayList<String>> processTrendAndDurationData;
	
	public StatisticHelper(){
		this.id_index = 0;
		this.timestamp_index = 1;
		this.activity_index = 2;
		this.time_pattern = "MM/dd/yyyy HH:mm:ss";
	}
	
	public StatisticHelper(String tp){
		this.id_index = 0;
		this.timestamp_index = 1;
		this.activity_index = 2;
		this.time_pattern = tp;
	}
	
	public StatisticHelper(int id_idx, int ts_idx, int act_idx, String tp){
		this.id_index = id_idx;
		this.timestamp_index = ts_idx;
		this.activity_index = act_idx;
		this.time_pattern = tp;
	}
	
	public void setNodeFilter(String beginname, String hasname){
		this.filterStartNodeName = beginname;
		this.filterHasNodeName = hasname;
		if(filterStartNodeName == null && filterHasNodeName == null){
			this.filterPath = false;			
		}else{
			this.filterPath = true;
		}
	}
	
	public void setAttrFilter(Map<Integer, String> attr){
		this.filterAttributes = attr;
		if(filterAttributes == null || filterAttributes.size() == 0){
			this.filterAttr = false;
		}else{
			this.filterAttr = true;
		}
	}
	
	public static Date getDateFromMillis(Long value){
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis(value);
		//System.out.println(c.getTime());
		return c.getTime();
	}
	
	private String timeAndUnit(Double milliseconds){
		StringBuffer result = new StringBuffer();
		if(milliseconds > YEAR_THRESHOLD*6){
			result.append(milliseconds/YEAR_THRESHOLD+YEAR_UNIT);
		} else if(milliseconds > DAY_THRESHOLD){
			result.append(milliseconds/DAY_THRESHOLD+DAY_UNIT);
		} else if(milliseconds > HOUR_THRESHOLD){
			result.append(milliseconds/HOUR_THRESHOLD+HOUR_UNIT);
		} else if(milliseconds > MIN_THRESHOLD){
			result.append(milliseconds/MIN_THRESHOLD+MIN_UNIT);
		} else if(milliseconds > SEC_THRESHOLD){
			result.append(milliseconds/SEC_THRESHOLD+SEC_UNIT);
		} else {
			result.append(milliseconds+MILISEC_UNIT);
		}
		return result.toString();
	}
	
	private Long getTimeStamp(String timestamp) throws ParseException{
		//SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.US);
		//SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
		SimpleDateFormat format = new SimpleDateFormat(this.time_pattern);
		//1970-01-01T12:09:00.000+01:00
		Date date = new Date();
        date=format.parse(timestamp); 
		return date.getTime();
	}
	
	private Long getActivityDurationTime(Path process, int i){
		if(i == 0)
			return 0L;
		else
			return process.path.get(i).timestamp - process.path.get(i-1).timestamp;
	}
	
	private Long getActivityLeadTime(Path process, int i){
		if(i == 0)
			return 0L;
		else
			return process.path.get(i).timestamp - process.path.get(0).timestamp;
	}
	
	private void setTrendAndDurationTime(ArrayList<String> trendAndDurationDataItem, Path process_instance, int index){
		trendAndDurationDataItem.add(String.valueOf(this.getActivityLeadTime(process_instance, index)/1000));
		trendAndDurationDataItem.add(String.valueOf(this.getActivityBeginTime(process_instance, index)));
		trendAndDurationDataItem.add(getDateFromMillis(this.getActivityBeginTime(process_instance, index)).toString());
	}
	
	private Long getActivityBeginTime(Path process, int i){
		return process.path.get(i).timestamp;
	}
	
	private Long getProcessDurationTime(Path process){
			return process.path.get(process.path.size()-1).timestamp - process.path.get(0).timestamp;
	}
	
	protected ArrayList<ArrayList<String>> getTrendAndDurationData(){
		return this.trendAndDurationData;
	}
	
	protected ArrayList<ArrayList<String>> getProcessTrendAndDurationData(){
		return this.processTrendAndDurationData;
	}
	
	public String calculate(String sample_id){
		ArrayList<ArrayList<String>> data = samples.get(sample_id);
		return  this.calculate(data);
	}
	
	public String calculatePredictive(){
		StringBuffer result = new StringBuffer();
		Map<String, Double> processOutcomes = new HashMap<String, Double>();
		Map<String, Double> startNodes = new HashMap<String, Double>();
		Double totalOutcomeInstance = 0.0;
		Map<String, Map<String, Double>> activityTransitions = new HashMap<String, Map<String, Double>>();
		Map<String, Double> totalTransitionInstance = new HashMap<String, Double>();
		DecimalFormat formatter = new DecimalFormat("0.000");
		for(Entry<Path, PathDetail> entry : pathDetails.entrySet()){
			Path path = entry.getKey();
			PathDetail pathDetail = entry.getValue();			
			String startNode = path.path.get(0).activity;
			String endNode = path.path.get(path.path.size()-1).activity;
			totalOutcomeInstance += pathDetail.instance_count;
			if(processOutcomes.get(endNode) == null){
				processOutcomes.put(endNode, pathDetail.instance_count);
			} else {
				processOutcomes.put(endNode, processOutcomes.get(endNode)+pathDetail.instance_count);
			}
			if(startNodes.get(startNode) == null){
				startNodes.put(startNode, pathDetail.instance_count);
			} else {
				startNodes.put(startNode, startNodes.get(startNode)+pathDetail.instance_count);
			}
			for(int i=0; i<path.path.size()-1; i++){
				String nodeName = path.path.get(i).activity;
				String nextNodeName = path.path.get(i+1).activity;
				Map<String, Double> transitions = activityTransitions.get(nodeName);
				if(transitions == null){
					transitions = new HashMap<String, Double>();
					transitions.put(nextNodeName, pathDetail.instance_count);
					activityTransitions.put(nodeName, transitions);
					totalTransitionInstance.put(nodeName, pathDetail.instance_count);
				} else if(transitions.get(nextNodeName) == null){
					transitions.put(nextNodeName, pathDetail.instance_count);
					totalTransitionInstance.put(nodeName, totalTransitionInstance.get(nodeName)+ pathDetail.instance_count);
				} else {
					transitions.put(nextNodeName, transitions.get(nextNodeName) + pathDetail.instance_count);
					totalTransitionInstance.put(nodeName, totalTransitionInstance.get(nodeName)+ pathDetail.instance_count);
				}
			}
		}
		result.append(PREDICTIVE_RESULT);
		for(Entry<String, Double> entry : processOutcomes.entrySet()){
			String outcome = entry.getKey();
			Double predict = entry.getValue()/totalOutcomeInstance;
			result.append(outcome+" : "+formatter.format(predict)+"\n");
		}
		result.append("'Start' Transitions:\n");
		for(Entry<String, Double> entry : startNodes.entrySet()){
			String node = entry.getKey();
			Double predict = entry.getValue()/totalOutcomeInstance;
			result.append(node+" : "+formatter.format(predict)+"\n");
		}
		result.append("\n");
		for(Entry<String, Map<String, Double>> entry : activityTransitions.entrySet()){
			String activityName = entry.getKey();
			Map<String, Double> transitions = entry.getValue();
			result.append("'"+activityName+"' Transitions:\n");
			for(Entry<String, Double> subentry : transitions.entrySet()){
				String outcomeName = subentry.getKey();
				Double predict = subentry.getValue()/totalTransitionInstance.get(activityName);
				result.append(outcomeName+" : "+formatter.format(predict)+"\n");
			}
			result.append("\n");
		}
		return result.toString();
	}
	
	public String calculate(ArrayList<ArrayList<String>> sample){
		StringBuffer result = new StringBuffer();
		int event_count = 0;
		//get allPaths
		for(ArrayList<String> event : sample){
			if(this.filterAttr){
				for(Entry<Integer, String> entry : this.filterAttributes.entrySet()){
					if(!event.get(entry.getKey()).equals(entry.getValue()))
						continue;
				}
			}
			String path_id = null;
			String act_name = null;
			Long timestamp;
			try {
				timestamp = this.getTimeStamp(event.get(this.timestamp_index));
				act_name = event.get(this.activity_index);
				path_id = event.get(this.id_index);
			} catch (Exception e) {				
				log_.warn("Fail to parse event, jump this item. Detail: "+ e.getMessage());				
				continue;
			}
			event_count++;
			ArrayList<String> attributes = new ArrayList<String>();
			for(int i=3; i<event.size(); i++){
				attributes.add(event.get(i));
			}
			Node node = new Node(act_name, attributes, timestamp);
			if(allPaths.get(path_id) == null){
				allPaths.put(path_id, new Path(node, path_id));
			} else {
				allPaths.get(path_id).insert(node);
			}
		}
		//filter the path by start node name
		if(this.filterPath){
			Iterator<Entry<String, Path>> i = allPaths.entrySet().iterator();
			while(i.hasNext()){
				//String key = (String)i.next();
				Path process_instance = ((Entry<String, Path>)i.next()).getValue();
				if(this.filterHasNodeName != null && this.filterStartNodeName != null){
					if(process_instance.beginWith(this.filterStartNodeName)
							&& process_instance.hasNode(filterHasNodeName)){
						continue;
					}
					//process_instance.cutPathByGivenHead(filterStartNodeName);
				}else if(this.filterStartNodeName != null){
					if(process_instance.beginWith(this.filterStartNodeName)){
						continue;
					}					
				}else if(this.filterHasNodeName != null){
					if(process_instance.hasNode(filterHasNodeName)){
						continue;
					}
				}
				event_count = event_count - process_instance.path.size();
				i.remove();
			}
		}
		//prepare to save some statistic results
		trendAndDurationData = new ArrayList<ArrayList<String>>();
		ArrayList<String> head = new ArrayList<String>();
		head.add("Path");
		head.add("Activity");
		head.add("Lead Time(s)");
		head.add("Begin Time(ms)");
		head.add("Begin Date");
		trendAndDurationData.add(head);
		
		processTrendAndDurationData = new ArrayList<ArrayList<String>>();
		ArrayList<String> head1 = new ArrayList<String>();
		head1.add("Path");
		head1.add("Process Duration(s)");
		head1.add("Process Begin Time(ms)");
		head1.add("Process Begin Date(s)");
		processTrendAndDurationData.add(head1);
		
		//get pathDetails
		for(Entry<String, Path> entry : allPaths.entrySet()){
			Path process_instance = entry.getValue();
			String path_info = process_instance.toString();
			ArrayList<String> trendAndDurationDataItem = null;
			ArrayList<String> processTrendAndDurationDataItem = null;
			Long processDuration = this.getProcessDurationTime(process_instance);
			Long processTrend = this.getActivityBeginTime(process_instance, 0);
			PathDetail pathDetail = null;
			if(pathDetails.get(process_instance) == null){
				Map<String, ActivityDetailsOnPath> activityDetailsOnPath = new HashMap<String, ActivityDetailsOnPath>();
				pathDetail = new PathDetail();
				for(int i=0; i<process_instance.path.size(); i++){
					ActivityDetailsOnPath activityDetail = null;
					String act_name = process_instance.path.get(i).activity;
					if(activityDetailsOnPath.get(act_name) == null){
						activityDetail = new ActivityDetailsOnPath(getActivityDurationTime(process_instance, i));
						activityDetailsOnPath.put(act_name, activityDetail);
					} else {
						activityDetailsOnPath.get(act_name).increase(getActivityDurationTime(process_instance, i));
					}
					trendAndDurationDataItem = new ArrayList<String>();
					trendAndDurationDataItem.add(path_info);
					trendAndDurationDataItem.add(act_name);
					setTrendAndDurationTime(trendAndDurationDataItem, process_instance, i);
					trendAndDurationData.add(trendAndDurationDataItem);
					pathDetail.event_count ++;
				}							
				//pathDetail.timecost += processDuration;
				pathDetail.activityDetails = activityDetailsOnPath;
				//pathDetail.instanceIDs.add(process_instance.instance_id);
				pathDetails.put(process_instance, pathDetail);
			} else {
				//another process instance in the same path
				pathDetail = pathDetails.get(process_instance);
				Map<String, ActivityDetailsOnPath> activityDetailsOnPath = pathDetail.activityDetails;  
				for(int i=0; i<process_instance.path.size(); i++){
					String act_name = process_instance.path.get(i).activity;
					activityDetailsOnPath.get(act_name).increase(getActivityDurationTime(process_instance, i));
					trendAndDurationDataItem = new ArrayList<String>();
					trendAndDurationDataItem.add(path_info);
					trendAndDurationDataItem.add(act_name);
					setTrendAndDurationTime(trendAndDurationDataItem, process_instance, i);
					trendAndDurationData.add(trendAndDurationDataItem);
					pathDetail.event_count ++;
				}
				//pathDetail.instance_count ++;
				//processDuration = this.getProcessDurationTime(process_instance);				
			}
			pathDetail.instanceIDs.add(process_instance.instance_id);
			pathDetail.instance_count ++;
			pathDetail.timecost += processDuration;
			processTrendAndDurationDataItem = new ArrayList<String>();
			processTrendAndDurationDataItem.add(path_info);
			processTrendAndDurationDataItem.add(String.valueOf(processDuration/1000));
			processTrendAndDurationDataItem.add(String.valueOf(processTrend));
			processTrendAndDurationDataItem.add(getDateFromMillis(processTrend).toString());
			processTrendAndDurationData.add(processTrendAndDurationDataItem);
		}
		//Calculation begin
		Map<String, Long> activity_duration_sum = new HashMap<String, Long>();
		Map<String, Double> activity_duration_count = new HashMap<String, Double>();
		Map<String, Double> frequency_activity_sum = new HashMap<String, Double>();
		Long durationOfAllProcess = 0L;
		result.append("Duration and Frequency values on selected path:\n");
		int idx = 1;
		for(Entry<Path, PathDetail> entry : pathDetails.entrySet()){
			ArrayList<String> visitedNodes = new ArrayList<String>();
			Path path = entry.getKey();
			PathDetail pathDetail = entry.getValue();
			Map<String, ActivityDetailsOnPath> activityDetails = pathDetail.activityDetails;
			result.append(idx+". "+path+"\n");
			result.append("Path Summary:\n");
			result.append("Events Processed: "+pathDetail.event_count+"\n");
			result.append("Process Discovered: "+pathDetail.instance_count+" (instanceIds:"+pathDetail.instanceIDs+")"+"\n");
			result.append("Total Average Duration: "+timeAndUnit(pathDetail.timecost/pathDetail.instance_count)+"\n");
			durationOfAllProcess += pathDetail.timecost;
			for(Node node : path.path){
				String act_name =  node.activity;
				if(visitedNodes.indexOf(act_name) != -1)
					 continue;
				 visitedNodes.add(act_name);
				 result.append(act_name+":\n");
				 ActivityDetailsOnPath activityDetail = activityDetails.get(act_name);
				 result.append("	Duration is "+timeAndUnit(activityDetail.duration.cost/activityDetail.duration.count)+"("+activityDetail.duration.cost/activityDetail.duration.count+MILISEC_UNIT+")\n");
				 result.append("	Duration percentage is "+(activityDetail.duration.cost/activityDetail.duration.count)/(pathDetail.timecost/pathDetail.instance_count)+"\n");
				 result.append("	Frequency is "+activityDetail.frequency.count+"\n");
				 result.append("	Frequency percentage is "+activityDetail.frequency.count/allPaths.size()+"\n\n");
				 if(activity_duration_sum.containsKey(act_name)){
					 activity_duration_sum.put(act_name, activity_duration_sum.get(act_name)+activityDetail.duration.cost);
					 activity_duration_count.put(act_name, activity_duration_count.get(act_name)+activityDetail.duration.count);
					 frequency_activity_sum.put(act_name,  frequency_activity_sum.get(act_name)+activityDetail.frequency.count);
				 } else {
					 activity_duration_sum.put(act_name, activityDetail.duration.cost);
					 activity_duration_count.put(act_name, activityDetail.duration.count);
					 frequency_activity_sum.put(act_name, activityDetail.frequency.count);
				 }
			}
			idx++;
		}
		result.append("Duration and Frequency values when no path is selected:\n");
		idx = 1;
		for(Entry<String, Long> entry : activity_duration_sum.entrySet()){
			 String act_name = entry.getKey();
			 result.append(idx+". "+act_name+"\n");
			 result.append("	Duration is "+timeAndUnit(entry.getValue()/activity_duration_count.get(act_name))+"("+entry.getValue()/activity_duration_count.get(act_name)+MILISEC_UNIT+")\n");
			 result.append("	Duration percentage is "+(entry.getValue()/activity_duration_count.get(act_name))/(durationOfAllProcess/allPaths.size())+"\n");
			 result.append("	Frequency is "+frequency_activity_sum.get(act_name)+"\n");
			 result.append("	Frequency percentage is "+frequency_activity_sum.get(act_name)/allPaths.size()+"\n\n");
			 idx++;
		}
		result.insert(0, StatisticHelper.RESULT
	    		.replace("{event_num}", String.valueOf(event_count))
	    		.replace("{process_num}", String.valueOf(allPaths.size()))
	    		.replace("{path_num}", String.valueOf(pathDetails.size()))
	    		.replace("{process_avg}",  timeAndUnit(durationOfAllProcess*1.0/allPaths.size())+"("+durationOfAllProcess*1.0/allPaths.size()+MILISEC_UNIT+")")
	    	);
		result.append(this.calculatePredictive());
		log_.debug(result.toString());
		if(result!=null && !result.equals(""))
			log_.info("Create statistic result successfully");
		return result.toString();
	}
}
